package com.fxu.framework.biz.util;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.jwt.JWTUtil;
import com.fxu.framework.core.holder.RedisHolder;
import com.fxu.framework.core.util.MapUtil;
import lombok.AllArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * 登录登出获取用户信息基础类
 *
 * @author fangxu
 * @version 1.0.0
 */
public final class LoginUtil {
	private final static int ONLINE_HOUR = 24; // 24小时过期
	private final static String ACCESS_TOKEN = "token";
	private final static String TOKEN_TIME = "tokenTime";
	private final static String TOKEN_KEY = "LoginKey";
	private final static String TOKEN_USER = "LoginUser";
	private final static String TOKEN_HOUR = "LoginHour";
	public final static String TOKEN_KEY_NAME = "name";

	private static RedisTemplate<String, Object> redisTemplate;
	private static RedisTemplate<String, Object> getRedisTemplate() {
		if (redisTemplate == null) {
			redisTemplate = RedisHolder.getRedisTemplate();
		}
		return redisTemplate;
	}

	/**
	 * <p>用户登录</p>
	 * @param userKey 用户Key[不同系统的id相同，可以增加特殊标识区分]
	 * @param userName 用户名[用户获取操作者]
	 * @param info 用户信息
	 * @return 登录返回过期时间
	 */
	public static long loginIn(String userKey, String userName, Map<String, Object> info) {
		return loginIn(userKey, userName, null, null, null, info);
	}

	/**
	 * <p>用户登录</p>
	 * @param userKey 用户Key[不同系统的id相同，可以增加特殊标识区分]
	 * @param userName 用户名[用户获取操作者]
	 * @param info 用户信息
	 * @param onlineHour 在线时间[小时]
	 * @return 登录返回过期时间
	 */
	public static long loginIn(String userKey, String userName, Map<String, Object> info, int onlineHour) {
		return loginIn(userKey, userName, null, null, null, info, onlineHour);
	}

	/**
	 * <p>用户登录</p>
	 * @param userKey 用户Key[不同系统的id相同，可以增加特殊标识区分]
	 * @param userName 用户名[用户获取操作者]
	 * @param roles 用户角色列表
	 * @param perms 用户权限列表
	 * @param menus 用户菜单列表
	 * @param info 用户信息
	 * @return 登录过期时间
	 */
	public static long loginIn(String userKey, String userName, Set<String> roles, Set<String> perms,
							   List<?> menus, Map<String, Object> info) {
		return loginIn(userKey, userName, roles, perms, menus, info, ONLINE_HOUR);
	}

	/**
	 * <p>用户登录</p>
	 * @param userKey 用户Key[不同系统的id相同，可以增加特殊标识区分]
	 * @param userName 用户名[用户获取操作者]
	 * @param roles 用户角色列表
	 * @param perms 用户权限列表
	 * @param menus 用户菜单列表
	 * @param info 用户信息
	 * @param onlineHour 在线时间[小时]
	 * @return 登录过期时间
	 */
	public static long loginIn(String userKey, String userName, Set<String> roles, Set<String> perms, List<?> menus,
							   Map<String, Object> info, int onlineHour) {
		Map<String, Object> map = new HashMap<>();
		map.put("key", userKey);
		map.put(TOKEN_KEY_NAME, userName);
		String token = JWTUtil.createToken(map, RandomUtil.randomString(10).getBytes());
		return loginInByToken(userKey, token, roles, perms, menus, info, onlineHour);
	}

	/**
	 * <p>用户登录</p>
	 * @param userKey 用户Key[不同系统的id相同，可以增加特殊标识区分]
	 * @param token 登录密钥
	 * @param roles 用户角色列表
	 * @param perms 用户权限列表
	 * @param menus 用户菜单列表
	 * @param info 用户信息
	 * @param onlineHour 在线时间[小时]
	 * @return 登录过期时间
	 */
	public static long loginInByToken(String userKey, String token, Set<String> roles, Set<String> perms, List<?> menus,
							   Map<String, Object> info, int onlineHour) {
		String tokenKey = getTokenKey(token);
		checkAndOut(userKey); // 如果存在，则退出登录
		long tokenTime = DateUtil.offsetHour(new Date(), onlineHour).getTime();
		info.put(ACCESS_TOKEN, token);
		info.put(TOKEN_TIME, tokenTime);
		info.put(TOKEN_HOUR, onlineHour);
		info.put(TOKEN_USER, userKey);
		if (!CollUtil.isEmpty(roles)) {
			getRedisTemplate().opsForHash().put(tokenKey, LoginKey.ROLES.name(), roles);
		}
		if (!CollUtil.isEmpty(perms)) {
			getRedisTemplate().opsForHash().put(tokenKey, LoginKey.PERMS.name(), perms);
		}
		if (!CollUtil.isEmpty(menus)) {
			getRedisTemplate().opsForHash().put(tokenKey, LoginKey.MENUS.name(), menus);
		}
		getRedisTemplate().opsForValue().set(getUserKey(userKey), token, onlineHour, TimeUnit.HOURS);
		getRedisTemplate().opsForHash().put(tokenKey, LoginKey.INFO.name(), info);
		getRedisTemplate().expire(tokenKey, onlineHour, TimeUnit.HOURS);
		return tokenTime;
	}

	/**
	 * <p>检查是否存在并登出</p>
	 * @param userKey 用户Key[不同系统的id相同，可以增加特殊标识区分]
	 */
	private static void checkAndOut(String userKey) {
		if (!StrUtil.isEmpty(userKey)) {
			loginOut(getToken(userKey));
		}
	}

	/**
	 * <p>获取用户登录钥匙</p>
	 * @param userKey 用户ID
	 * @return 用户登录钥匙
	 */
	public static String getToken(String userKey) {
		if (StrUtil.isEmpty(userKey)) return null;
		Object token = getRedisTemplate().opsForValue().get(getUserKey(userKey));
		return null == token ? null : (String) token;
	}

	/**
	 * <p>获取用户菜单列表</p>
	 * @param token 登录钥匙
	 * @return 用户菜单列表
	 */
	@SuppressWarnings("unchecked")
	public static List<Object> getMenus(String token) {
		Object value = get(token, LoginKey.MENUS);
		return null == value ? null : (List<Object>) value;
	}

	/**
	 * <p>获取用户权限列表</p>
	 * @param token 登录钥匙
	 * @return 用户权限列表
	 */
	@SuppressWarnings("unchecked")
	public static Set<String> getPerms(String token) {
		Object value = get(token, LoginKey.PERMS);
		return null == value ? null : (Set<String>) value;
	}

	/**
	 * <p>获取用户角色列表</p>
	 * @param token 登录钥匙
	 * @return 用户角色列表
	 */
	@SuppressWarnings("unchecked")
	public static Set<String> getRoles(String token) {
		Object value = get(token, LoginKey.ROLES);
		return null == value ? null : (Set<String>) value;
	}

	/**
	 * <p>获取用户信息</p>
	 * @param token 登录钥匙
	 * @return 用户信息
	 */
	@SuppressWarnings("unchecked")
	public static Map<String, Object> getInfo(String token) {
		Object value = get(token, LoginKey.INFO);
		return null == value ? null : (Map<String, Object>) value;
	}

	/**
	 * <p>获取用户信息</p>
	 * @param token 登录钥匙
	 * @return 用户信息
	 */
	public static boolean isLogin(String token) {
		return isLogin(token, false);
	}

	/**
	 * <p>获取用户信息</p>
	 * @param token 登录钥匙
	 * @param expire 是否更新过期时间
	 * @return 用户信息
	 */
	public static boolean isLogin(String token, boolean expire) {
		if (StrUtil.isEmpty(token)) return false;
		String tokenKey = getTokenKey(token);
		if (expire) { // 过期时间延长
			Map<String, Object> info = getInfo(token);
			if (info == null) return false;
			int onlineHour = MapUtil.getInt(info, TOKEN_HOUR, ONLINE_HOUR);
			long tokenTime = DateUtil.offsetHour(new Date(), onlineHour).getTime();
			long expireTime = MapUtil.getLong(info, TOKEN_TIME, System.currentTimeMillis());
			// 10分钟才更新一次
			if (tokenTime - expireTime > 1000 * 60 * 10) {
				info.put(TOKEN_TIME, tokenTime); // 更新过期时间
				String userKey = MapUtil.getStr(info, TOKEN_USER, "");
				getRedisTemplate().opsForValue().set(getUserKey(userKey), token, onlineHour, TimeUnit.HOURS);
				getRedisTemplate().opsForHash().put(tokenKey, LoginKey.INFO.name(), info);
				getRedisTemplate().expire(tokenKey, onlineHour, TimeUnit.HOURS);
			}
		}
		return getRedisTemplate().opsForHash().hasKey(tokenKey, LoginKey.INFO.name());
	}

	/**
	 * <p>更新用户信息</p>
	 * @param token 登录钥匙
	 * @param info  用户信息
	 */
	public static void flushInfo(String token, Map<String, Object> info) {
		if (StrUtil.isEmpty(token)) return;
		getRedisTemplate().opsForHash().put(getTokenKey(token), LoginKey.INFO.name(), info);
	}
	
	/**
	 * <p>根据userId和token登出</p>
	 * @param userKey 用户ID
	 * @param token 登录钥匙
	 * @return 退出登录是否成功
	 */
	public static boolean loginOut(String userKey, String token) {
		if (StrUtil.isEmpty(token)) {
			return false;
		}
    	if (!StrUtil.isEmpty(userKey)) {
    		getRedisTemplate().delete(getUserKey(userKey));
    	}
		return Boolean.TRUE.equals(getRedisTemplate().delete(getTokenKey(token)));
	}
	
	/**
	 * <p>登出</p>
	 * @param token 登录钥匙
	 * @return 退出登录是否成功
	 */
	public static boolean loginOut(String token) {
		return loginOut(null, token);
	}
	
	private static Object get(String token, LoginKey key) {
		return StrUtil.isEmpty(token) ? null : getRedisTemplate().opsForHash().get(getTokenKey(token), key.name());
	}

	private static String getUserKey(String userKey) {
		return TOKEN_KEY + "_U_" + userKey;
	}
	
	private static String getTokenKey(String token) {
		// : token.length() <= 32 ? token
		return TOKEN_KEY + "_" + (StrUtil.isEmpty(token) ? "" : SecureUtil.md5(token));
	}

	@AllArgsConstructor
	enum LoginKey {
		ROLES("角色"),
		MENUS("菜单"),
		PERMS("权限"),
		INFO("信息");
		final String desc;
	}
}